在軟體開發中,錯誤就像是在吃魚的時候碰到魚刺:你知道這種情況遲早會發生,但總希望自己不會碰到。
每個程式語言都有一套自己的防錯工具,而 Rust 的方法非常有趣:它透過 Result
和 Option
這兩個枚舉來讓開發者去思考該如何面對這個問題,不但幫助你應對各種錯誤狀況,還能讓程式在運行時更安全。
上篇我們有介紹了枚舉,也有提到了關於Option
跟Result
,熟悉如何在 Rust 開發過程善這兩個工具來提升程式開發時的自我掌握度和可讀性是很重要的。所以本篇希望透過其他例子來加深這兩個工具應用的印象。
Option
:找不到的東西也不會讓程式崩潰Option
在 Rust 中的角色很簡單:它就像一個小盒子,有時候裡面有東西(Some
),有時候是空的(None
)。這個機制讓我們不用擔心會不小心處理到空值而導致程式出錯。
想像你正在開發一個功能,要在一個清單中尋找特定的元素,這裡我們可以用 Option
來處理「找得到」和「找不到」的兩種情況,這樣做的好處是讓程式可以針對每種狀況做出適當的反應,而不是直接崩潰。
// 定義一個函數 find_element,接收一個整數陣列 arr 和一個目標整數 target,並指定回傳 Option<usize>
// Option<usize> 表示回傳的結果可能是某個索引值(Some),也可能是沒有找到(None)
fn find_element(arr: &[i32], target: i32) -> Option<usize> {
// 使用迴圈遍歷 arr 切片中的每個元素及其索引
for (index, &value) in arr.iter().enumerate() {
// 如果當前元素與目標相符
if value == target {
return Some(index); // 找到目標,回傳索引
}
}
None // 沒有找到,回傳 None
}
fn main() {
// 定義一個整數陣列 numbers
let numbers = [10, 20, 30, 40, 50];
// 設定目標值為 30
let target = 30;
// 使用 match 語句來匹配 find_element 的回傳結果
match find_element(&numbers, target) {
// 如果找到,印出目標和索引
Some(index) => println!("找到 {} 在索引位置 {}", target, index),
// 如果找不到,印出找不到的訊息
None => println!("找不到目標 {}", target),
}
}
find_element
函數回傳 Option<usize>
,如果找到了目標值,就回傳 Some(index)
,表示目標的位置;如果找不到,則回傳 None
。main
函數中,我們使用 match
來根據返回的結果進行處理,不管是找到還是找不到,都能安全地處理。如果不使用 Option
,你可能會寫出讓程式報錯的程式碼,如直接嘗試對可能不存在的索引進行操作。Option
讓你在程式中清楚地處理這些有可能發生的「空值」情況,避免了不必要的崩潰。
Result
:操作可能失敗?別擔心,事先處理就好Result
是 Rust 中另一個常見的錯誤處理工具,它的運作模式類似於 Option
,但更進一步地表達了「成功」或「失敗」的狀況,並附上詳細的訊息。可以想像它是一個通知包裹,打開包裹後,你會發現它不是送錯了,就是送對了,而且還會告訴你詳細的情況。
假設你要讀取一個檔案的內容,但有可能檔案不存在、沒有權限讀取等等。這裡用 Result
來處理這種成功或失敗的情況,是再適合不過的了。
// 引入標準庫中的 File 結構和 io 模組,用於檔案操作和 I/O 處理
use std::fs::File;
use std::io::{self, Read};
// 定義一個函數 read_file_content,接收檔案路徑的字串切片,回傳 Result<String, io::Error>
// Result<String, io::Error> 表示操作可能成功並返回字串內容,或失敗並返回 I/O 錯誤
fn read_file_content(path: &str) -> Result<String, io::Error> {
// 嘗試開啟指定路徑的檔案
// File::open(path) 會返回 Result<File, io::Error>
// 使用 ? 操作符,若開啟失敗,錯誤會自動傳遞給呼叫者
let mut file = File::open(path)?;
// 創建一個新的空字串,用於儲存檔案的內容
let mut content = String::new();
// 使用 read_to_string 方法將檔案內容讀入 content 字串中
// 同樣使用 ? 操作符處理可能的 I/O 錯誤
file.read_to_string(&mut content)?;
// 若以上操作都成功,則返回 Ok 並包裝檔案內容的字串
Ok(content)
}
fn main() {
// 定義要讀取的檔案路徑
let file_path = "example.txt";
// 使用 match 語句來處理 read_file_content 的回傳結果
match read_file_content(file_path) {
// 如果成功(即回傳 Ok),則打印檔案內容
Ok(content) => println!("文件內容:\n{}", content),
// 如果失敗(即回傳 Err),則打印錯誤訊息
Err(error) => println!("讀取文件失敗:{}", error),
}
}
使用 Result
表達成功與失敗:
Result<String, io::Error>
是一個泛型類別,表達這個操作的結果可以是成功,並攜帶字串內容(Ok(String)
),或是失敗並攜帶錯誤訊息(Err(io::Error)
)。?
操作符的作用:
?
操作符是 Rust 中一個非常方便的錯誤處理機制。如果函數遇到 Err
或者None
,它會立即返回這個錯誤給呼叫者,而不需要明確使用 return
。?
會自動將結果帶入,讓程式繼續往下執行。詳細的流程:
File::open(path)?
用來開啟指定的檔案路徑,若開啟失敗,則回傳為錯誤。file.read_to_string(&mut content)?
將檔案內容讀入 content
字串中,若失敗同樣會傳遞錯誤。Ok(content)
,即檔案的內容,並將content返回。match
語句處理 Result
:
match
來根據 Result
的回傳結果決定下一步動作。若為 Ok
,則顯示檔案內容;若為 Err
,則顯示錯誤訊息,避免程式崩潰。使用 Result
,你能確保每次的操作都考慮到了失敗的可能性。與其讓程式在發生問題後才崩潰,不如事先做好萬全準備,讓使用者知道為什麼操作失敗了,而且用一個 ?
就可以相當於 python 的 try
except
作用,讓程式碼更簡潔。
unwrap
、expect
、map
和 and_then
Rust 提供了幾種方便的方式來操作 Result
和 Option
,讓程式碼更加簡潔且易於處理錯誤。以下介紹幾個常見的用法及其詳細的操作。
unwrap
和 expect
我們先來看一個如何使用 unwrap
與 expect
的例子
use std::fs::File;
use std::io::Write;
fn main() {
// 範例 1: 使用 `unwrap` 打開已知存在的檔案
// 我們假設這個檔案已經存在且可讀取
let mut file = File::create("example.txt").unwrap(); // 假設檔案建立一定成功,不會 panic
writeln!(file, "Hello, Rust!").unwrap(); // 寫入檔案,如果操作成功,這樣可以簡潔地執行
// 範例 2: 使用 `expect` 處理已驗證過的數字轉換
let number_str = "42"; // 假設這個字串是經過驗證的
let number: i32 = number_str.parse().expect("轉換為整數時出錯"); // 轉換一定成功,不會 panic
println!("轉換成功的數字是:{}", number);
// 範例 3: 使用 `expect` 處理已經確認不會失敗的選項
let some_value = Some(100); // 已知這裡的值一定是 Some
let value = some_value.expect("值應該存在但卻不見了"); // 因為確定是 Some,所以 expect 不會 panic
println!("取出的值是:{}", value);
}
在上面的例子中,範例1的程式內由於 let mut file = File::create("example.txt")
在沒有加入 unwrap
之前,其實是會回傳 Result
的,所以理論上還需要透過 match
等判斷方式來看回傳的 Result
是 Ok
還是 Err
,才能進一步取值。
let mut file = match File::create("example.txt") {
Ok(f) => f,
Err(e) => {
println!("建立檔案失敗: {}", e);
return;
}
};
因此如果直接使用 unwrap
就能夠在回傳的 Result<T, E>
為 Ok
時,直接代入檔案建立成功的結果。這樣的操作在回傳值為 Option
枚舉的情況下同樣適用,這樣可以增加編寫時的便利性。
但嘗試透過 unwrap
從 Option
或 Result
中取出值是有一定風險的,如果回傳的變體是 None
或 Err
,程式會直接 panic,也就是例外跳出
或崩潰
。因此使用 unwrap
適合在非常確信不會出錯的情況下使用來取值,否則仍是以 match
方式分別處理較合適。
而範例2跟範例3,都使用了 expect
,那麼有用跟沒有用差別在哪裡呢?
其中如果不加上expect
,直接寫 let number: i32 = number_str.parse();
,則這樣定義其實是會出錯的,因為 number_str.parse()
的回傳值會是 Result<T, E>
,因此同範例1,這裡的number也還需要透過 match
或者是回傳的變體判斷才能進一步取值。
然而,當我們的回傳值為 Option
或者 Result
的情況下,加上 expect
的使用也就代表著,如果不是 None
或者 Err
的話就把成功結果回傳,否則就打印出自定義的錯誤訊息。由於上面沒有發生panic,所以得到的結果如下圖:
expect
:與 unwrap
功能相似,但可以自定義錯誤訊息。在錯誤發生時,會顯示自定義的錯誤訊息,這對於除錯非常有幫助。fn main() {
// 定義一個 `Some(10)` 的 Option 變數
let some_value = Some(10);
// 定義一個 `None` 的 Option 變數,類別為 `Option<i32>`
let none_value: Option<i32> = None;
// 使用 `unwrap()` 從 `some_value` 中取出值
println!("值:{}", some_value.unwrap()); // 輸出 10
// 嘗試使用 `unwrap()` 從 `none_value` 中取值,若為 `None` 會直接 panic
// println!("值:{}", none_value.unwrap()); // 若執行這行,程式會 panic,顯示錯誤訊息
// 定義一個 `Err` 的 Result 變數
let result: Result<i32, &str> = Err("操作失敗");
// 使用 `expect()`,若出錯會顯示自訂錯誤訊息
result.expect("期待成功但卻失敗了"); // 顯示自訂錯誤訊息 "期待成功但卻失敗了"
}
在上面的結果中可以看到 expect
的作用,也就是會在遇到panic時打印出自定義的錯誤訊息,因此將有助於我們發現是在哪裡遇到了 None
或者 Err
。
unwrap
跟 expect
,如何選擇?既然我們知道 unwrap
跟 expect
的目的都是方便我們遇到 Option
或者 Result
的時候取值使用,而 expect
又可以更進一步自定義錯誤訊息,那為什麼我們不直接都使用 expect
就好了呢?
其實這樣理解是正確的,但是在長期的開發情況下,如果每次都要使用 expect
並且後面要加上 "" 或者其他自定義錯誤訊息,也是會影響整體開發體驗的。所以這就端看各位開發者自己的選擇囉,但個人是覺得如果可以接受 expect
後面能不加上字串設定,那就太好了。
map
和 and_then
map
:用來對 Some
或 Ok
的值進行操作,並返回一個新的 Option
或 Result
。它可以讓你在保持原始結構的情況下,對其中的值進行處理。
and_then
:用於串接多個操作,特別是在你需要連續處理 Option
或 Result
時非常實用。and_then
會接收一個返回 Option
或 Result
的函數,讓你能夠更方便地鏈式處理多個步驟。
fn main() {
// 定義一個 `Some(5)` 的 Option 變數
let some_value = Some(5);
// 使用 `map` 對 `Some(5)` 進行乘 2 操作,返回 `Some(10)`
let new_value = some_value.map(|x| x * 2); // 將 5 乘以 2
// 輸出 `Some(10)`
println!("{:?}", new_value); // 輸出 Some(10)
// 使用 `and_then` 來連續處理 Option
let value = Some(4).and_then(square_if_even); // 4 是偶數,返回 Some(16)
// 輸出 `Some(16)`
println!("{:?}", value); // 輸出 Some(16)
}
// 定義一個函數 `square_if_even`,接受一個整數並返回 Option<i32>
fn square_if_even(x: i32) -> Option<i32> {
// 判斷是否為偶數
if x % 2 == 0 {
Some(x * x) // 是偶數,返回平方值
} else {
None // 不是偶數,返回 None
}
}
map
的使用場景:
map
用於對 Some
或 Ok
中的值進行處理,而不影響原本的結構。這讓程式碼更具表現力,尤其是在需要對多個 Option
或 Result
值進行不同操作時。and_then
的作用:
and_then
可以理解為「然後做什麼」的操作。當前一個操作成功時,會繼續進行下一步,這在連續多步驟的處理中非常方便,可以大幅減少冗長的 match
語句。Result
or Option
?讀者看到這邊應該可以發現,Rust程式開發當中,我們會需要處理大量的 Option
與 Result
的判斷,因此,不僅僅是我們需要在我們原創的程式碼中善用兩者 ,也要能夠判斷引用的套件或其他程式碼內容當中,是返回哪一種以及如何去應對。
Option
和 Result
結合起來用假設你在開發一個程式,需要同時處理 I/O 錯誤和數學運算錯誤,這裡我們可以用 Result
和 Option
的組合來應對各種狀況。
use std::fs::File;
use std::io::{self, Read};
fn read_file_content(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
fn safe_divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
fn main() {
// 文件讀取範例
let file_path = "example.txt";
match read_file_content(file_path) {
Ok(content) => println!("文件內容:\n{}", content),
Err(error) => println!("讀取文件失敗:{}", error),
}
// 安全除法範例
let a = 10.0;
let b = 0.0;
match safe_divide(a, b) {
Some(result) => println!("除法結果:{}", result),
None => println!("錯誤:分母不能為 0"),
}
}
在 Rust 中, Result
和 Option
是兩個Rust開發中常見的枚舉,熟悉應對與巧秒運用,則將能夠使自己更能夠掌握程式的運作過程中對不同情況發生的反應,並且依據可能的情況進行排除。
對我自己而言,雖然Python的 try except
可以很簡單的略過錯誤情況而使程式不至於遇到panic終止,但是由於它的特性,造成 Silent Errors 的情況,反而隱藏重要的錯誤原因,這情況也是時常可能發生的。
而Rust的 ?
就等同於是簡化版的 try except
,適合在一個段落裡多次呼叫,又不佔空間也很直觀,這是我認為最好用的部分之一。
以上就是一些關於 Result
和 Option
的相關操作,不知道有沒有人跟我一樣,到後來才發現 expect
!= except
,太習慣python的錯誤處理方式了。